﻿using System;
using System.Linq;
using System.Reflection;
using AdvancedGod.Generation;

namespace AdvancedGod.People.Couples
{
  public class ChildCreator
  {
    #region private consts
    /// <summary>
    /// Expected name of field with surname
    /// </summary>
    private const string ExpectedSurnamePropertyName = "Surname";
    /// <summary>
    /// Expected name of field with sex
    /// </summary>
    private const string ExpectedSexPropertyName = "Sex";
    /// <summary>
    /// Name of package in which humans classes locates
    /// </summary>
    private const string HumanTypesPackage = "AdvancedGod.People.";
    #endregion

    #region private static vars
    /// <summary>
    /// Single instance of class
    /// </summary>
    private static ChildCreator instance;
    #endregion

    #region public static properties
    /// <summary>
    /// Getter for single instance of class
    /// </summary>
    public static ChildCreator Instance
    {
      get { return instance ?? (instance = new ChildCreator()); }
    }
    #endregion

    #region private methods
    /// <summary>
    /// Default constructor (hided)
    /// </summary>
    private ChildCreator()
    {
    }

    /// <summary>
    /// Finds type of expected child of two humans
    /// </summary>
    /// <param name="firstHuman">first parent</param>
    /// <param name="secondHuman">second parent</param>
    /// <exception cref="NoAttributeException">throws if there is no attribute 
    /// with pertient pair</exception>
    /// <returns>type of child to be created</returns>
    private Type FindChildType(Human firstHuman, Human secondHuman)
    {
      CoupleAttribute pertientAttribute =
        CouplingUtils.FindPertientCoupleAttribute(firstHuman, secondHuman);
      string childTypeName = pertientAttribute.ChildType;
      return Type.GetType(HumanTypesPackage + childTypeName);
    }

    /// <summary>
    /// Finds info about first method returning string
    /// </summary>
    /// <param name="human">human whose search method</param>
    /// <returns>info about pertient method if exists, null otherwise</returns>
    private MethodInfo FindFirstMethodReturningString(Human human)
    {
      MethodInfo[] methodInfos = human.GetType().GetMethods();
      return methodInfos.FirstOrDefault(t => t.ReturnType == typeof(string));
    }

    /// <summary>
    /// Sets surname if child have property "Surname" accordingly to parents
    /// </summary>
    /// <param name="child">child which exposes surname</param>
    /// <param name="firstHuman">first parent</param>
    /// <param name="secondHuman">second parent</param>
    private void TrySetSurname(IHasName child, Human firstHuman, Human secondHuman)
    {
      Type childType = child.GetType();
      PropertyInfo childSurnameProperty =
        childType.GetProperty(ExpectedSurnamePropertyName);
      if (childSurnameProperty != null)
      {
        string manParentName = firstHuman.Sex == Sex.Male ? firstHuman.Name : secondHuman.Name;
        PropertyInfo childSexProperty = childType.GetProperty(ExpectedSexPropertyName);
        var childSex = (Sex)childSexProperty.GetValue(child, null);
        string generatedSurname =
          RandomGenerator.Instance.GenerateSurname(manParentName, childSex);
        childSurnameProperty.SetValue(child, generatedSurname, null);
      }
    }
    #endregion

    #region public methods
    /// <summary>
    /// Creates new child from two humans
    /// </summary>
    /// <param name="firstHuman">first human</param>
    /// <param name="secondHuman">second human</param>
    /// <exception cref="NoAttributeException">throws if there is 
    /// no attribute with pertient pair</exception>
    /// <returns>new child</returns>
    public IHasName Create(Human firstHuman, Human secondHuman)
    {
      IHasName child = null;
      MethodInfo methodInfo = FindFirstMethodReturningString(secondHuman);
      try
      {
        var newName = methodInfo.Invoke(secondHuman, null) as string;
        Type childType = FindChildType(firstHuman, secondHuman);
        var arguments = new[] { newName };
        child = Activator.CreateInstance(childType, arguments) as IHasName;
        TrySetSurname(child, firstHuman, secondHuman);
      }
      catch (ArgumentException)
      {
      }
      return child;

    }
    #endregion


  }
}
